﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Pfz.Extensions;
using Pfz.RemoteGaming.Internal;
using Pfz.Remoting;
using Pfz.Remoting.Udp;
using Pfz.Serialization;
using Pfz.Threading;

namespace Pfz.RemoteGaming
{
	/// <summary>
	/// Class used to connect to a remote game.
	/// </summary>
	public sealed class RemoteGameClient:
		ThreadSafeDisposable
	{
		#region Fields
			private IConnection _client;
			private Stream _stream;
			private Assembly[] _commonAssemblies;

			private readonly Dictionary<long, RemoteGameComponent> _components = new Dictionary<long, RemoteGameComponent>();
			private readonly Dictionary<Type, ConstructorInfo> _realConstructors = new Dictionary<Type, ConstructorInfo>();
			private readonly Assembly[] _clientAssemblies;
			private long _lastVolatileValuesVersion;
			private List<KeyValuePair<long, object>> _requestResults = new List<KeyValuePair<long, object>>();
		#endregion

		#region Constructor
			/// <summary>
			/// Creates a new game client, prepared to find the real implementations in the given
			/// clientAssemblies. If none is provided, the entry assembly is used.
			/// </summary>
			public RemoteGameClient(params Assembly[] clientAssemblies)
			{
				if (clientAssemblies == null || clientAssemblies.Length == 0)
					_clientAssemblies = new Assembly[]{Assembly.GetEntryAssembly()};
				else
					_clientAssemblies = clientAssemblies;
			}
		#endregion
		#region Dispose
			/// <summary>
			/// Releases all resources used by this object.
			/// </summary>
			protected override void Dispose(bool disposing)
			{
				if (disposing)
				{
					var waitingRequests = _waitingRequests;
					if (waitingRequests != null)
					{
						_waitingRequests = null;
						foreach(var request in waitingRequests.Values)
							request(null);
					}

					Disposer.Dispose(ref _event);
					Disposer.Dispose(ref _stream);
					Disposer.Dispose(ref _client);

					Changed = null;
					ExceptionThrown = null;
				}

				base.Dispose(disposing);
			}
		#endregion

		#region Properties
			#region _ClientLock
				internal object _ClientLock
				{
					get
					{
						return base.DisposeLock;
					}
				}
			#endregion

			#region Connection
				/// <summary>
				/// Gets the Connection used this by Game Client.
				/// You can use it to identify if an exception was thrown by a connection lost or by another reason.
				/// </summary>
				public IConnection Connection
				{
					get
					{
						return _client;
					}
				}
			#endregion
			#region ClientId
				private long _clientId;
				/// <summary>
				/// Gets the client id of this client.
				/// Server components created for this client will only reference it by the Id.
				/// </summary>
				public long ClientId
				{
					get
					{
						return _clientId;
					}
				}
			#endregion
		#endregion
		#region Methods
			#region _ClientReader
				private void _ClientReader()
				{
					try
					{
						var context = new _RemoteGameContext();
						context._lock = DisposeLock;
						context._components = _components;

						var binarySerializer = new BinarySerializer();
						binarySerializer.AddRecommendedDefaults();
						binarySerializer.AddDefaultTypeRecursive(typeof(_ServerChangesInfo));
						binarySerializer.AddDefaultTypeRecursive(typeof(_ClientChangesInfo));

						binarySerializer.Context = new StreamingContext(StreamingContextStates.All, context);
						var initializationData = (_InitializationData)binarySerializer.Deserialize(_stream);
						_Initialize(initializationData);

						initializationData.AddDefaultTypes(binarySerializer);

						var unguaranteedConnection = _client as IUdpConnection;
						if (unguaranteedConnection != null)
						{
							var unguaranteedSerializer = new BinarySerializer();
							unguaranteedSerializer.AddRecommendedDefaults();
							unguaranteedSerializer.AddDefaultTypeRecursive(typeof(_ComponentChanges));
							unguaranteedSerializer.AddDefaultTypeRecursive(typeof(_VolatileValues));
							initializationData.AddDefaultTypes(unguaranteedSerializer);
							_udpSerializer = unguaranteedSerializer;

							UnlimitedThreadPool.Run(_UdpReceiver, initializationData);
						}

						UnlimitedThreadPool.Run(_ClientWriter, initializationData);

						while(true)
						{
							context._ownerId = _clientId;
							object serverChanges = binarySerializer.Deserialize(_stream);

							var typedServerChanges = (_ServerChangesInfo)serverChanges;
							_ApplyServerChanges(typedServerChanges);
						}
					}
					catch(Exception exception)
					{
						if (!WasDisposed)
							if (!_OnException(exception))
								throw;
					}
					finally
					{
						Dispose();
					}
				}
			#endregion
			#region _ClientWriter
				private void _ClientWriter(_InitializationData initializationData)
				{
					try
					{
						var binarySerializer = new BinarySerializer();
						binarySerializer.AddRecommendedDefaults();
						binarySerializer.AddDefaultTypeRecursive(typeof(_ServerChangesInfo));
						binarySerializer.AddDefaultTypeRecursive(typeof(_ClientChangesInfo));

						initializationData.AddDefaultTypes(binarySerializer);

						while(true)
						{
							var clientChanges = _GetClientChanges();
							if (clientChanges == null)
								return;

							binarySerializer.Serialize(_stream, clientChanges);
							_stream.Flush();
						}
					}
					catch(Exception exception)
					{
						if (!WasDisposed)
							if (!_OnException(exception))
								throw;
					}
					finally
					{
						Dispose();
					}
				}
			#endregion

			#region _UdpReceiver
				private BinarySerializer _udpSerializer;
				private void _UdpReceiver(_InitializationData initializationData)
				{
					var client = _client;
					if (client == null)
						return;

					var connection = client as IUdpConnection;
					if (connection == null)
						return;

					try
					{
						var deserializer = new BinarySerializer();
						deserializer.AddRecommendedDefaults();
						deserializer.AddDefaultTypeRecursive(typeof(_ComponentChanges));
						deserializer.AddDefaultTypeRecursive(typeof(_VolatileValues));
						initializationData.AddDefaultTypes(deserializer);

						while(!WasDisposed)
						{
							byte[] received = connection.UdpReceive();
							if (received == null)
								return;

							var changesDictionary = new RemoteGameComponentChanges();
							object deserialized;
							using(var stream = new MemoryStream(received))
							{
								stream.Position = connection.UdpHeaderSize;
								while(stream.Position < stream.Length)
								{
									deserialized = deserializer.Deserialize(stream);

									var volatileValues = deserialized as _VolatileValues;
									if (volatileValues != null)
									{
										lock(DisposeLock)
										{
											if (WasDisposed)
												return;

											_ApplyVolatileValues(volatileValues, changesDictionary);
										}
										continue;
									}

									var changes = (_ComponentChanges)deserialized;
									lock(DisposeLock)
									{
										if (WasDisposed)
											return;

										_ApplyComponentChanges(changes, false, changesDictionary);
									}
								}
							}

							changesDictionary._RemoveEmptyChanges();
							if (changesDictionary.Count > 0)
							{
								var handler = Changed;
								if (handler != null)
								{
									var args = new RemoteGameChangedEventArgs();
									args.ComponentChanges = changesDictionary;
									handler(this, args);
								}
							}
						}
					}
					catch(Exception exception)
					{
						if (!WasDisposed)
							if (!_OnException(exception))
								throw;
					}
					finally
					{
						Dispose();
					}
				}
			#endregion
			#region _ApplyVolatileValues
				private void _ApplyVolatileValues(_VolatileValues volatileValues, RemoteGameComponentChanges changesDictionary)
				{
					if (volatileValues == null)
						return;

					if (volatileValues._version <= _lastVolatileValuesVersion)
						return;

					foreach(var pair in volatileValues._values)
					{
						long componentId = pair.Key;
						RemoteGameComponent component;
						if (!_components.TryGetValue(componentId, out component))
							continue;

						var changes = changesDictionary._GetOrCreateValue(component);
						component._ApplyVolatileValues(pair.Value, changes);
					}
					_lastVolatileValuesVersion = volatileValues._version;
				}
			#endregion
			#region _ApplyServerChanges
				private void _ApplyServerChanges(_ServerChangesInfo changes)
				{
					var handler = Changed;
					RemoteGameComponent[] addedComponentsArray = null;
					List<RemoteGameComponent> removedComponentsList = null;
					RemoteGameComponentChanges changesDictionary = null;

					lock(DisposeLock)
					{
						CheckUndisposed();

						if (changes._participantId != _clientId)
						{
							_clientId = changes._participantId;

							int futureCount = _futureChanges.Count;
							for(int i=futureCount-1; i>=0; i--)
								if (_futureChanges[i]._participantId < _clientId)
									_futureChanges.RemoveAt(i);

							if (handler == null)
							{
								foreach(var component in _components.Values)
									component.Dispose();

								_components.Clear();
							}
							else
							{
								removedComponentsList = _components.Values.ToList();
								_components.Clear();
							}
						}

						var removedComponents = changes._removedComponents;
						if (removedComponents != null)
						{
							if (removedComponentsList == null)
								removedComponentsList = new List<RemoteGameComponent>(removedComponents.Length);

							foreach(var id in removedComponents)
							{
								var component = _components.GetValueOrDefault(id);
								if (component != null)
								{
									removedComponentsList.Add(component);
									_components.Remove(id);
								}
							}
						}

						var addedComponents = changes._addedComponents;
						if (addedComponents != null)
						{
							addedComponentsArray = new RemoteGameComponent[addedComponents.Length];

							int index = -1;
							foreach(var pair in addedComponents)
							{
								var commonType = pair.Key;
								var id = pair.Value;

								var constructorInfo = _GetConstructorInfo(commonType);
								var result = (RemoteGameComponent)constructorInfo.Invoke(null);
								result._roomLock = DisposeLock;
								result._id = id;
								result._client = this;
								_components.Add(id, result);

								index++;
								addedComponentsArray[index] = result;
							}
						}

						var componentChanges = changes._componentChanges;
						var volatileValues = changes._volatileValues;

						if (componentChanges != null || volatileValues != null)
						{
							changesDictionary = new RemoteGameComponentChanges();
							_ApplyComponentChanges(componentChanges, true, changesDictionary);
							_ApplyVolatileValues(volatileValues, changesDictionary);
						}

						if (addedComponentsArray != null)
							foreach(var component in addedComponentsArray)
								component.OnInitialize();

						var requestResults = changes._requestResults;
						if (requestResults != null)
						{
							foreach(var pair in requestResults)
							{
								var requestId = pair.Key;
								var action = _waitingRequests[requestId];
								_waitingRequests.Remove(requestId);
								action(pair.Value);
							}
						}

						var request = changes._request;
						if (request != null)
						{
							lock(_registeredActions)
							{
								object result = null;

								Func<RemoteGameRequest, object> action;
								if (!_registeredActions.TryGetValue(request.GetType(), out action))
									throw new RemoteGameException("Server requested an action of type " + request.GetType().FullName + " which is unsupported by this client.");

								result = action(request);

								if (request._expectedResultId != 0)
								{
									var pair = new KeyValuePair<long, object>(request._expectedResultId, result);
									_requestResults.Add(pair);
								}

								if (_requestResults.Count > 0)
									_event.Set();
							}
						}
					}

					if (handler != null)
					{
						var args = new RemoteGameChangedEventArgs();

						if (addedComponentsArray != null)
							args.AddedComponents = new ReadOnlyCollection<RemoteGameComponent>(addedComponentsArray);

						if (removedComponentsList != null)
							args.RemovedComponents = new ReadOnlyCollection<RemoteGameComponent>(removedComponentsList);

						if (changesDictionary != null)
						{
							changesDictionary._RemoveEmptyChanges();

							if (changesDictionary.Count > 0)
								args.ComponentChanges = changesDictionary;
						}

						handler(this, args);
					}

					if (removedComponentsList != null)
						foreach(var component in removedComponentsList)
							component.Dispose();
				}
			#endregion
			#region _ApplyComponentChanges
				private long _lastProcessedChangesVersion;
				private bool _ApplyComponentChanges(_ComponentChanges componentChanges, bool applyIfRepeated, RemoteGameComponentChanges changesDictionary)
				{
					if (componentChanges == null)
					{
						if (applyIfRepeated)
						{
							_ApplyFutureChangesImmediately(changesDictionary);
							return true;
						}

						return false;
					}

					long version = componentChanges._version;
					if (version <= _lastProcessedChangesVersion)
					{
						if (applyIfRepeated)
						{
							_ApplyFutureChangesImmediately(changesDictionary);
							return true;
						}

						return false;
					}

					if (componentChanges._participantId != _clientId && !applyIfRepeated)
					{
						foreach(var futureModification in _futureChanges)
						{
							if (version < futureModification._version)
							{
								if (applyIfRepeated)
								{
									_ApplyFutureChangesImmediately(changesDictionary);
									return true;
								}
								return false;
							}
						}
					}
					else
					{
						foreach(var futureModification in _futureChanges)
						{
							if (futureModification._version == version)
							{
								if (applyIfRepeated)
								{
									_ApplyFutureChangesImmediately(changesDictionary);
									return true;
								}
					
								return false;
							}
						}
					}

					_futureChanges.Add(componentChanges);
					_ApplyFutureChangesImmediately(changesDictionary);
					return true;
				}
			#endregion
			#region _ApplyFutureChangesImmediately
				private List<_ComponentChanges> _futureChanges = new List<_ComponentChanges>();
				private void _ApplyFutureChangesImmediately(RemoteGameComponentChanges changesDictionary)
				{
					int count = _futureChanges.Count;
					if (count == 0)
						return;

					if (count > 1)
						_futureChanges.Sort((a, b) => a._version.CompareTo(b._version));

					RemoteGameProperty._propertiesLock.EnterReadLock();
					try
					{
						bool ok = true;
						foreach(var componentChanges in _futureChanges)
						{
							foreach (var pair in componentChanges._changes)
							{
								var componentId = pair.Key;
								var changes = pair.Value;

								var component = _components.GetValueOrDefault(componentId);
								if (component == null)
								{
									ok = false;
									continue;
								}

								var valueChanges = changesDictionary._GetOrCreateValue(component);
								component._componentLock.EnterWriteLock();
								try
								{
									if (!component._wasDisposed)
									{
										var dictionary = component._values;
										foreach (var pair2 in changes)
										{
											var oldValue = dictionary.GetValueOrDefault(pair2.Key);

											var value = pair2.Value;
											if (value == null)
												dictionary.Remove(pair2.Key);
											else
												dictionary[pair2.Key] = value;

											var property = RemoteGameProperty._properties[pair2.Key];
											var change = new RemoteGamePropertyChange(oldValue, value);
											valueChanges._Set(property, change);
										}
									}
								}
								finally
								{
									component._componentLock.ExitWriteLock();
								}
							}

							if ((ok || componentChanges._participantId < _clientId) && componentChanges._version == _lastProcessedChangesVersion+1)
								_lastProcessedChangesVersion++;
						}
					}
					finally
					{
						RemoteGameProperty._propertiesLock.ExitReadLock();
					}

					int countToDelete = count;
					for(int i=0; i<count; i++)
					{
						if (_futureChanges[i]._version > _lastProcessedChangesVersion)
						{
							countToDelete = i;
							break;
						}
					}

					if (countToDelete > 0)
						_futureChanges.RemoveRange(0, countToDelete);
				}
			#endregion

			#region _Initialize
				private void _Initialize(_InitializationData initializationData)
				{
					lock(DisposeLock)
					{
						CheckUndisposed();

						_commonAssemblies = initializationData._commonAssemblies;

						RemoteGameProperty._propertiesLock.EnterWriteLock();
						try
						{
							_AssemblyGenerator._propertiesDictionary.Clear();
							RemoteGameProperty._properties.Clear();
							var remotePropertyInfos = initializationData._propertyInfos;
							RemoteGameProperty._properties.AddRange(remotePropertyInfos);

							foreach(var remotePropertyInfo in remotePropertyInfos)
								_AssemblyGenerator._propertiesDictionary.Add(remotePropertyInfo.GetPropertyInfo(), remotePropertyInfo);
						}
						finally
						{
							RemoteGameProperty._propertiesLock.ExitWriteLock();
						}
					}
				}
			#endregion
			#region _OnException
				private bool _OnException(Exception exception)
				{
					var handler = ExceptionThrown;
					if (handler == null)
						return false;

					var args = new RemoteGameExceptionEventArgs();
					args.Exception = exception;
					handler(this, args);
					return args.WasHandled;
				}
			#endregion
			#region _GetConstructorInfo
				private ConstructorInfo _GetConstructorInfo(Type commonType)
				{
					ConstructorInfo result;
					if (_realConstructors.TryGetValue(commonType, out result))
						return result;

					Type realType = null;
					foreach(var assembly in _clientAssemblies)
					{
						var possibleResults = commonType.GetSubClassesRecursive(assembly);
						int length = possibleResults.Count;
						if(length == 0)
							continue;

						if (length > 1 || realType != null)
							throw new RemoteGameException("More than one possible implementation was found for " + commonType.FullName);

						realType = possibleResults[0];
					}

					if (realType == null)
						realType = commonType;

					result = _AssemblyGenerator.GetConstructor(realType);
					_realConstructors.Add(commonType, result);

					return result;
				}
			#endregion
			#region _GetClientChanges
				private long _componentChangesVersion;
				internal ManagedAutoResetEvent _event = new ManagedAutoResetEvent(false);
				private _ClientChangesInfo _GetClientChanges()
				{
					var localEvent = _event;
					if (localEvent == null)
						return null;

					while(true)
					{
						localEvent.WaitOne();

						List<KeyValuePair<long, KeyValuePair<int, object>[]>> changes;
						lock(DisposeLock)
						{
							if (WasDisposed)
								return null;

							changes = new List<KeyValuePair<long, KeyValuePair<int, object>[]>>();

							foreach(var component in _components.Values)
							{
								var key = component._id;
								var value = component._GetChanges();

								if (value != null)
								{
									var pair = new KeyValuePair<long, KeyValuePair<int, object>[]>(key, value);
									changes.Add(pair);
								}
							}
						}

						KeyValuePair<long, object>[] requestResults = null;
						lock(_registeredActions)
						{
							if (_requestResults.Count > 0)
							{
								requestResults = _requestResults.ToArray();
								_requestResults.Clear();
							}
						}

						_ComponentChanges componentChanges = null;
						if (changes.Count > 0 || _requests.Count > 0 || requestResults != null)
						{
							if (changes.Count > 0)
							{
								_componentChangesVersion++;
								componentChanges = new _ComponentChanges(_componentChangesVersion, _clientId, changes.ToArray());

								var udpConnection = _client as IUdpConnection;
								if (udpConnection != null)
								{
									var serializer = _udpSerializer;
									if (serializer != null)
									{
										using(var stream = new MemoryStream())
										{
											serializer.Serialize(stream, componentChanges);
											if (stream.Length <= 1024)
											{
												var guaranteedDelivery = udpConnection as IGuaranteedUdpConnection;
												if (guaranteedDelivery != null)
												{
													guaranteedDelivery.GuaranteedSend(stream.GetBuffer(), 0, (int)stream.Length);
													componentChanges = null;
												}
												else
													udpConnection.UdpSend(stream.GetBuffer(), 0, (int)stream.Length);
											}
										}
									}
								}
							}

							RemoteGameRequest request = null;
							int requestCount = _requests.Count;
							if (requestCount > 0)
							{
								request = _requests.Dequeue();

								if (requestCount > 1)
									localEvent.Set();
							}

							if (componentChanges == null && request == null && requestResults == null)
								continue;

							var result = new _ClientChangesInfo();
							result._clientId = _clientId;
							result._changes = componentChanges;
							result._request = request;
							result._requestResults = requestResults;
							return result;
						}
					}
				}
			#endregion
			#region _GenerateNotificationIfNeeded
				internal bool _notificationsEnabled = true;
				internal void _GenerateNotificationIfNeeded()
				{
					lock(DisposeLock)
					{
						if (WasDisposed)
							return;

						if (_notificationsEnabled)
							_event.Set();
					}
				}
			#endregion

			#region Start
				#region By Host and Port
					/// <summary>
					/// Starts a game connecting to the given host and port.
					/// </summary>
					public void Start(string host, int port)
					{
						var client = new TcpWithUdpConnection(host, port, false, 8*1024);
						Start(client);
					}
				#endregion
				#region By Connection
					/// <summary>
					/// Starts a game using the given client.
					/// </summary>
					public void Start(IConnection connection)
					{
						if (connection == null)
							throw new ArgumentNullException("connection");

						lock(DisposeLock)
						{
							CheckUndisposed();

							if (_stream != null)
								throw new RemoteGameException("This RemoteGameClient was already started.");

							_client = connection;
							var stream = _client.Stream;
							_stream = stream;
							UnlimitedThreadPool.Run(_ClientReader);
						}
					}
				#endregion
			#endregion
			#region GetComponents
				/// <summary>
				/// Gets all components in this client.
				/// </summary>
				/// <returns>An array with all components in this Client or null if this client was disposed.</returns>
				public RemoteGameComponent[] GetComponents()
				{
					lock(DisposeLock)
					{
						if (WasDisposed)
							return null;

						return _components.Values.ToArray();
					}
				}
			#endregion
			#region Request
				private Queue<RemoteGameRequest> _requests = new Queue<RemoteGameRequest>();
				private long _expectedResultId;
				private Dictionary<long, Action<object>> _waitingRequests = new Dictionary<long, Action<object>>();
				/// <summary>
				/// Send a request to the server. Use this when the action cannot be lost 
				/// (for example, changing a property to 1 and then to 2 can only send 2 to the server, 
				/// while a Request with value one and a Request with value two can be received in the same
				/// frame but both will be send to the server).
				/// </summary>
				public void Request(RemoteGameRequest request, Action<object> onResult=null)
				{
					if (request == null)
						throw new ArgumentNullException("request");

					lock(DisposeLock)
					{
						CheckUndisposed();

						if (onResult != null)
						{
							_expectedResultId++;
							request._expectedResultId = _expectedResultId;
							_waitingRequests.Add(_expectedResultId, onResult);
						}

						_requests.Enqueue(request);

						if (_notificationsEnabled)
							_event.Set();
					}
				}
			#endregion
			#region SyncRequest
				/// <summary>
				/// Requests an action from the server and waits until it responds.
				/// Do not call this from the Changed event or OnInitialize method of your 
				/// game components or you will cause a dead-lock.
				/// </summary>
				public object SyncRequest(RemoteGameRequest request)
				{
					object result = null;
					using(var waitEvent = new ManagedManualResetEvent())
					{
						Request
						(
							request, 
							(localResult) =>
							{
								result = localResult;
								waitEvent.Set();
							}
						);
				
						waitEvent.WaitOne();
						return result;
					}
				}
			#endregion

			#region GetImplementedType
				/// <summary>
				/// Gets the implemented type for a common/client abstract game type.
				/// </summary>
				public Type GetImplementedType(Type abstractComponentType)
				{
					if (abstractComponentType == null)
						throw new ArgumentNullException("abstractComponentType");

					if (!abstractComponentType.IsAbstract)
						throw new ArgumentException("type is not abstract.", "abstractComponentType");

					if (!typeof(RemoteGameComponent).IsAssignableFrom(abstractComponentType))
						throw new ArgumentException("type must be a RemoteGameComponent type.", "abstractComponentType");

					lock(DisposeLock)
					{
						CheckUndisposed();

						var constructorInfo = _GetConstructorInfo(abstractComponentType);
						return constructorInfo.DeclaringType;
					}
				}
			#endregion

			#region BeginUpdates
				/// <summary>
				/// Call this method when making lots of changes.
				/// Notifications to the server will be disabled until you dispose the returned
				/// object.
				/// </summary>
				public IDisposable BeginUpdates()
				{
					return new _NotificationsDisablerForClient(this);
				}
			#endregion
			#region RegisterAction - Overloads
				internal readonly Dictionary<Type, Func<RemoteGameRequest, object>> _registeredActions = new Dictionary<Type, Func<RemoteGameRequest, object>>();

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<T, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameRequest, object>
						(
							(request) => action((T)request)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _ClientUnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameRequest, object>
						(
							(request) => action()
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _ClientUnregisterAction(this, typeof(T));
				}
			#endregion
		#endregion
		#region Events
			#region Changed
				/// <summary>
				/// Event invoked when components are added, removed or their properties are changed.
				/// </summary>
				public event EventHandler<RemoteGameChangedEventArgs> Changed;
			#endregion
			#region ExceptionThrown
				/// <summary>
				/// Event invoked when an exception is thrown. The connection will be lost in either case,
				/// but you may want to avoid the exception from crashing the application.
				/// </summary>
				public event EventHandler<RemoteGameExceptionEventArgs> ExceptionThrown;
			#endregion
		#endregion
	}
}
